﻿// <copyright file="Network.cs" company="City of Portland, BES-ASM">
// </copyright>
// <summary>Network class</summary>

namespace BESASM.EMGAATS.BusinessClasses
{
  using System;
  using System.Collections.Generic;
  using System.Collections.Specialized;
  using System.ComponentModel;
  using System.Linq;
  using System.Text;
  using System.Threading.Tasks;

  /// <summary>
  /// Network class
  /// </summary>
  public class Network : INetwork
  {
    private Dictionary<Type, int> nextId = new Dictionary<Type, int>();
    private List<Type> entityTypes = new List<Type>();

    private bool isOptimized = false;
    private Dictionary<EntityType, List<string>> optimizedProperties = null;
    private ILookup<INode, ILink> upstreamLinks = null;
    private ILookup<IArea, IDirector> areaDirectors = null;
    private Dictionary<INode, List<IArea>> upstreamAreas = null;
    private ILookup<INode, IWeir> attachedWeirs = null;
    private ILookup<INode, IOrifice> attachedOrifices = null;
    private ILookup<INode, IStorage> attachedStorage = null;

    /// <summary>
    /// Initializes a new instance of the <see cref="Network" /> class.
    /// </summary>
    public Network()
    {
      this.Nodes = new EntityCollection<INode>(this);
      this.Links = new EntityCollection<ILink>(this);
      this.CrossSections = new EntityCollection<ICrossSection>(this);
      this.Weirs = new EntityCollection<IWeir>(this);
      this.Orifices = new EntityCollection<IOrifice>(this);
      this.Areas = new EntityCollection<IArea>(this);
      this.Directors = new EntityCollection<IDirector>(this);
      this.Infiltrators = new EntityCollection<IInfiltrator>(this);
      this.Storages = new EntityCollection<IStorage>(this);

      this.Nodes.CollectionChanged += this.EntityCollection_CollectionChanged;
      this.Links.CollectionChanged += this.EntityCollection_CollectionChanged;
      this.CrossSections.CollectionChanged += this.EntityCollection_CollectionChanged;
      this.Weirs.CollectionChanged += this.EntityCollection_CollectionChanged;
      this.Orifices.CollectionChanged += this.EntityCollection_CollectionChanged;
      this.Areas.CollectionChanged += this.EntityCollection_CollectionChanged;
      this.Directors.CollectionChanged += this.EntityCollection_CollectionChanged;
      this.Infiltrators.CollectionChanged += this.EntityCollection_CollectionChanged;
      this.Storages.CollectionChanged += this.EntityCollection_CollectionChanged;

      this.InitializeOptimizedProperties();

      this.entityTypes.Add(typeof(INode));
      this.entityTypes.Add(typeof(ILink));
      this.entityTypes.Add(typeof(IWeir));
      this.entityTypes.Add(typeof(IOrifice));
      this.entityTypes.Add(typeof(IArea));
      this.entityTypes.Add(typeof(ICrossSection));
      this.entityTypes.Add(typeof(IDirector));
      this.entityTypes.Add(typeof(IInfiltrator));
      this.entityTypes.Add(typeof(IStorage));

      this.InitializeNextIds();
    }

    /// <summary>
    /// Gets the nodes of the Network
    /// </summary>
    public EntityCollection<INode> Nodes { get; private set; }

    /// <summary>
    /// Gets the weirs of the Network
    /// </summary>
    public EntityCollection<IWeir> Weirs { get; private set; }

    /// <summary>
    /// Gets the orifices of the Network
    /// </summary>
    public EntityCollection<IOrifice> Orifices { get; private set; }

    /// <summary>
    /// Gets the links of the Network
    /// </summary>
    public EntityCollection<ILink> Links { get; private set; }

    /// <summary>
    /// Gets the cross sections of the Network
    /// </summary>
    public EntityCollection<ICrossSection> CrossSections { get; private set; }

    /// <summary>
    /// Gets the areas of the Network
    /// </summary>
    public EntityCollection<IArea> Areas { get; private set; }

    /// <summary>
    /// Gets the directors of the Network
    /// </summary>
    public EntityCollection<IDirector> Directors { get; private set; }

    /// <summary>
    /// Gets the infiltrators of the Network
    /// </summary>
    public EntityCollection<IInfiltrator> Infiltrators { get; private set; }

    /// <summary>
    /// Gets the storages of the Network
    /// </summary>
    public EntityCollection<IStorage> Storages { get; private set; }

    /// <summary>
    /// Creates a node for the Network
    /// </summary>
    /// <returns>A reference to the created Node</returns>
    public INode CreateNode()
    {
      return this.InternalCreateEntity<INode>(this.nextId[typeof(INode)]--);
    }

    /// <summary>
    /// Create a node for the Network
    /// </summary>
    /// <param name="id">ID of the Node</param>
    /// <returns>A reference to the created Node</returns>
    public INode CreateNode(int id)
    {
      return this.InternalCreateEntity<INode>(id);
    }

    /// <summary>
    /// Create a Weir for the Network
    /// </summary>
    /// <returns>The created Weir</returns>
    public IWeir CreateWeir()
    {
      return this.InternalCreateEntity<IWeir>(this.nextId[typeof(IWeir)]--);
    }

    /// <summary>
    /// Create a Weir for the Network
    /// </summary>
    /// <param name="id">The ID of the Weir</param>
    /// <returns>The created Weir</returns>
    public IWeir CreateWeir(int id)
    {
      return this.InternalCreateEntity<IWeir>(id);
    }

    /// <summary>
    /// Create an Orifice for the Network
    /// </summary>
    /// <returns>The created Orifice</returns>
    public IOrifice CreateOrifice()
    {
      return this.InternalCreateEntity<IOrifice>(this.nextId[typeof(IOrifice)]--);
    }

    /// <summary>
    /// Create an Orifice for the Network
    /// </summary>
    /// <param name="id">The ID of the Orifice</param>
    /// <returns>The created Orifice</returns>
    public IOrifice CreateOrifice(int id)
    {
      return this.InternalCreateEntity<IOrifice>(id);
    }

    /// <summary>
    /// Create Link for the Network
    /// </summary>
    /// <returns>The created Link</returns>
    public ILink CreateLink()
    {
      return this.InternalCreateEntity<ILink>(this.nextId[typeof(ILink)]--);
    }

    /// <summary>
    /// Creates a Link for the Network
    /// </summary>
    /// <param name="id">ID of the Link</param>
    /// <returns>A reference to the created Link</returns>
    public ILink CreateLink(int id)
    {
      return this.InternalCreateEntity<ILink>(id);
    }

    /// <summary>
    /// Creates a cross section for the Network
    /// </summary>
    /// <returns>A reference to the created CrossSection</returns>
    public ICrossSection CreateCrossSection()
    {
      return this.InternalCreateEntity<ICrossSection>(this.nextId[typeof(ICrossSection)]--);
    }

    /// <summary>
    /// Creates a cross section for the Network
    /// </summary>
    /// <param name="id">ID of the CrossSection</param>
    /// <returns>A reference to the created CrossSection</returns>
    public ICrossSection CreateCrossSection(int id)
    {
      return this.InternalCreateEntity<ICrossSection>(id);
    }

    /// <summary>
    /// Creates an Area for the Network
    /// </summary>
    /// <returns>The created Area</returns>
    public IArea CreateArea()
    {
      return this.InternalCreateEntity<IArea>(this.nextId[typeof(IArea)]--);
    }

    /// <summary>
    /// Creates an Area for the Network
    /// </summary>
    /// <param name="id">The ID of the Area</param>
    /// <returns>The created Area</returns>
    public IArea CreateArea(int id)
    {
      return this.InternalCreateEntity<IArea>(id);
    }

    /// <summary>
    /// Creates a Director for the Network
    /// </summary>
    /// <returns>The created Director</returns>
    public IDirector CreateDirector()
    {
      return this.InternalCreateEntity<IDirector>(this.nextId[typeof(IDirector)]--);
    }

    /// <summary>
    /// Creates a Director for the Network
    /// </summary>
    /// <param name="id">The ID of the Director</param>
    /// <returns>The created Director</returns>
    public IDirector CreateDirector(int id)
    {
      return this.InternalCreateEntity<IDirector>(id);
    }

    /// <summary>
    /// Creates an Infiltrator for the Network
    /// </summary>
    /// <returns>The created Infiltrator</returns>
    public IInfiltrator CreateInfiltrator()
    {
      return this.InternalCreateEntity<IInfiltrator>(this.nextId[typeof(IInfiltrator)]--);
    }

    /// <summary>
    /// Creates an Infiltrator for the Network
    /// </summary>
    /// <param name="id">The ID of the Infiltrator</param>
    /// <returns>The created Infiltrator</returns>
    public IInfiltrator CreateInfiltrator(int id)
    {
      return this.InternalCreateEntity<IInfiltrator>(id);
    }

    /// <summary>
    /// Creates a Storage for the Network
    /// </summary>
    /// <returns>The created Storage</returns>
    public IStorage CreateStorage()
    {
      return this.InternalCreateEntity<IStorage>(this.nextId[typeof(IStorage)]--);
    }

    /// <summary>
    /// Creates a Storage for the Network
    /// </summary>
    /// <param name="id">The ID of the Storage</param>
    /// <returns>The created Storage</returns>
    public IStorage CreateStorage(int id)
    {
      return this.InternalCreateEntity<IStorage>(id);
    }

    /// <summary>
    /// Creates an Entity
    /// </summary>
    /// <typeparam name="T">The type of Entity</typeparam>
    /// <returns>The created Entity</returns>
    public T CreateEntity<T>() where T : IEntity
    {
      return this.InternalCreateEntity<T>(this.nextId[typeof(T)]--);
    }

    /// <summary>
    /// Creates an Entity for the Network
    /// </summary>
    /// <typeparam name="T">A class that implements IEntity</typeparam>
    /// <param name="id">The ID of the Entity</param>
    /// <returns>A reference to the created Entity</returns>
    public T CreateEntity<T>(int id) where T : IEntity
    {
      return this.InternalCreateEntity<T>(id);
    }

    /// <summary>
    /// Adds an entity to the network.
    /// </summary>
    /// <typeparam name="T">A class that implements IEntity</typeparam>
    /// <param name="entity">The entity to add to the network.</param>
    public void Add<T>(T entity) where T : IEntity
    {
      if (entity == null)
      {
        throw new ArgumentNullException("entity", "The entity being added to the network is null.");
      }

      EntityCollection<T> collection = this.GetEntityCollection(entity) as EntityCollection<T>;

      if (collection.Contains(entity.Id))
      {
        throw new InvalidOperationException(string.Format("The network already contains a {0} with the give Id.", entity.EntityType));
      }
      else
      {
        collection.Add(entity);
      }
    }

    /// <summary>
    /// Optimizes lookup of related (e.g. upstream) entities. Only valid until the network is modified.
    /// </summary>
    public void Optimize()
    {
      /* IF A NEW OPTIMIZATION COLLECTION IS ADDED, MAKE SURE TO ALSO UPDATE EntityCollection_CollectionChanged() and Entity_PropertyChanged() METHODS. */

      this.upstreamLinks = this.Links.ToLookup(link => link.DSNode);
      this.areaDirectors = this.Directors.ToLookup(director => director.Area);
      this.attachedWeirs = this.Weirs.ToLookup(weir => weir.USNode);
      this.attachedOrifices = this.Orifices.ToLookup(orifice => orifice.USNode);
      this.attachedStorage = this.Storages.ToLookup(storage => storage.Node);

      // TODO: Find a Linq way of doing this (may not exist--looks like Linq may not support many to many relationships)
      this.upstreamAreas = new Dictionary<INode, List<IArea>>();

      foreach (INode node in this.Nodes)
      {
        this.upstreamAreas[node] = new List<IArea>();
      }

      foreach (IDirector director in this.Directors)
      {
        if (director.ToNode != null && director.Area != null)
        {
          this.upstreamAreas[director.ToNode].Add(director.Area);
        }
      }

      this.isOptimized = true;
    }

    /// <summary>
    /// Clears the network of all entities
    /// </summary>
    public void Clear()
    {
      this.Nodes.Clear();
      this.Weirs.Clear();
      this.Orifices.Clear();
      this.Links.Clear();
      this.CrossSections.Clear();
      this.Areas.Clear();
      this.Directors.Clear();
      this.Infiltrators.Clear();
      this.Storages.Clear();

      this.InitializeNextIds();
    }

    /// <summary>
    /// Extracts a subnetwork of the network by copying all entities related to the given links into a new Network.
    /// </summary>
    /// <param name="linkIds">The IDs of the links to extract for making the subnetwork.</param>
    /// <returns>A new Network containing copies of all the specified links and associated entities.</returns>
    public INetwork ExtractSubnetwork(IList<int> linkIds)
    {
      if (linkIds == null)
      {
        throw new ArgumentNullException("linkIds");
      }

      List<ILink> links = new List<ILink>();
      foreach (int linkId in linkIds)
      {
        if (!this.Links.Contains(linkId))
        {
          throw new ArgumentException(string.Format("This network does not contain a link with Id = {0}", linkId));
        }

        links.Add(this.Links[linkId]);
      }

      return this.ExtractSubnetwork(links);
    }

    /// <summary>
    /// Updates the internal key for an Entity.
    /// </summary>
    /// <param name="entity">The Entity object.</param>
    /// <param name="id">The new key for the Entity</param>
    /// <remarks>This method updates the key for the entity in the EntityCollection. The Id of the entity should be updated to match.</remarks>
    internal void UpdateEntityKey(IEntity entity, int id)
    {
      switch (entity.EntityType)
      {
        case EntityType.Area:
          this.Areas.ChangeKey((IArea)entity, id);
          break;
        case EntityType.CrossSection:
          this.CrossSections.ChangeKey((ICrossSection)entity, id);
          break;
        case EntityType.Director:
          this.Directors.ChangeKey((IDirector)entity, id);
          break;
        case EntityType.Node:
          this.Nodes.ChangeKey((INode)entity, id);
          break;
        case EntityType.Link:
          this.Links.ChangeKey((ILink)entity, id);
          break;
        case EntityType.Weir:
          this.Weirs.ChangeKey((IWeir)entity, id);
          break;
        case EntityType.Orifice:
          this.Orifices.ChangeKey((IOrifice)entity, id);
          break;
        case EntityType.Infiltrator:
          this.Infiltrators.ChangeKey((IInfiltrator)entity, id);
          break;
        case EntityType.Storage:
          this.Storages.ChangeKey((IStorage)entity, id);
          break;
        default:
          throw new NotSupportedException(string.Format("Unsupported entity type: {0}", entity.EntityType));
      }
    }

    /// <summary>
    /// Gets the areas directly upstream of the given node.
    /// </summary>
    /// <param name="node">A node in the network.</param>
    /// <returns>The areas directly upstream of the given node.</returns>
    internal IEnumerable<IArea> GetUpstreamAreas(INode node)
    {
      if (!this.isOptimized)
      {
        this.Optimize();
      }

      List<IArea> areas;
      if (!this.upstreamAreas.TryGetValue(node, out areas))
      {
        areas = new List<IArea>();
      }

      return areas.Distinct();
    }

    /// <summary>
    /// Gets the directors associated with this area.
    /// </summary>
    /// <param name="area">The area to get directors for.</param>
    /// <returns>The directors associated with this area.</returns>
    internal IEnumerable<IDirector> GetAreaDirectors(IArea area)
    {
      if (!this.isOptimized)
      {
        this.Optimize();
      }

      return this.areaDirectors[area];
    }

    /// <summary>
    /// Gets the links directly upstream of a given node.
    /// </summary>
    /// <param name="node">A node in the network.</param>
    /// <returns>The links directly upstream of a node.</returns>
    internal IEnumerable<ILink> GetUpstreamLinks(INode node)
    {
      if (!this.isOptimized)
      {
        this.Optimize();
      }

      return this.upstreamLinks[node];
    }

    /// <summary>
    /// Gets the weirs attached to a given node.
    /// </summary>
    /// <param name="node">A node in the network</param>
    /// <returns>The weirs attached to a node.</returns>
    internal IEnumerable<IWeir> GetAttachedWeirs(INode node)
    {
      if (!this.isOptimized)
      {
        this.Optimize();
      }

      return this.attachedWeirs[node];
    }

    /// <summary>
    /// Gets the orifices attached to a given node.
    /// </summary>
    /// <param name="node">A node in the network</param>
    /// <returns>The orifices attached to a node</returns>
    internal IEnumerable<IOrifice> GetAttachedOrifices(INode node)
    {
      if (!this.isOptimized)
      {
        this.Optimize();
      }

      return this.attachedOrifices[node];
    }

    /// <summary>
    /// Gets the storage attached to a given node.
    /// </summary>
    /// <param name="node">A node in the network</param>
    /// <returns>The storage attached to a nodes</returns>
    internal IEnumerable<IStorage> GetAttachedStorage(INode node)
    {
      if (!this.isOptimized)
      {
        this.Optimize();
      }

      return this.attachedStorage[node];
    }

    private static void ResetIds(INetwork network)
    {
      ResetIds(network.Nodes);
      ResetIds(network.Weirs);
      ResetIds(network.Orifices);
      ResetIds(network.Links);
      ResetIds(network.CrossSections);
      ResetIds(network.Areas);
      ResetIds(network.Directors);
      ResetIds(network.Infiltrators);
      ResetIds(network.Storages);
    }

    private static void ResetIds<T>(ICollection<T> entityCollection) where T : IEntity
    {
      int id = -1;
      foreach (T entity in entityCollection)
      {
        if (entity.Id <= id)
        {
          id = entity.Id - 1;
        }
      }

      foreach (IEntity entity in entityCollection)
      {
        entity.Id = id--;
      }
    }

    private static void CloneLink(INetwork subnetwork, ILink link)
    {
      ILink linkClone = link.Clone() as ILink;

      if (link.USNode != null)
      {
        if (!subnetwork.Nodes.Contains(link.USNode.Id))
        {
          subnetwork.Add(link.USNode.Clone() as INode);
        }

        linkClone.USNode = subnetwork.Nodes[link.USNode.Id];
      }

      if (link.DSNode != null)
      {
        if (!subnetwork.Nodes.Contains(link.DSNode.Id))
        {
          subnetwork.Add(link.DSNode.Clone() as INode);
        }

        linkClone.DSNode = subnetwork.Nodes[link.DSNode.Id];
      }

      if (link.CrossSection != null)
      {
        if (!subnetwork.CrossSections.Contains(link.CrossSection.Id))
        {
          subnetwork.Add(link.CrossSection.Clone() as ICrossSection);
        }

        linkClone.CrossSection = subnetwork.CrossSections[link.CrossSection.Id];
      }

      subnetwork.Add(linkClone);
    }

    private static void CloneDirector(INetwork subnetwork, IDirector director)
    {
      if (director.ToNode != null && subnetwork.Nodes.Contains(director.ToNode.Id))
      {
        IDirector directorClone = director.Clone() as IDirector;
        directorClone.ToNode = subnetwork.Nodes[director.ToNode.Id];
        subnetwork.Add(directorClone);
      }
    }

    private static void CloneArea(INetwork subnetwork, IArea area)
    {
      bool cloneArea = false;

      // TODO: determine if we need OverlandFlowDestination
      if (area.OverlandFlowDestination != null && subnetwork.Nodes.Contains(area.OverlandFlowDestination.Id))
      {
        cloneArea = true;
      }

      if (!cloneArea)
      {
        foreach (IDirector director in area.Directors)
        {
          if (subnetwork.Directors.Contains(director.Id))
          {
            cloneArea = true;
            break;
          }
        }
      }

      if (cloneArea)
      {
        IArea areaClone = area.Clone() as IArea;

        if (area.OverlandFlowDestination != null && subnetwork.Nodes.Contains(area.OverlandFlowDestination.Id))
        {
          areaClone.OverlandFlowDestination = subnetwork.Nodes[area.OverlandFlowDestination.Id];
        }
        else
        {
          areaClone.OverlandFlowDestination = null;
        }

        subnetwork.Add(areaClone);

        foreach (IDirector director in area.Directors)
        {
          if (subnetwork.Directors.Contains(director.Id))
          {
            areaClone.AddDirector(subnetwork.Directors[director.Id]);
          }
          else
          {
            IDirector disconnectedDirector = director.Clone() as IDirector;
            disconnectedDirector.ToNode = null;
            subnetwork.Add(disconnectedDirector);

            areaClone.AddDirector(disconnectedDirector);
          }
        }
      }
    }

    private static void CloneWeir(INetwork subnetwork, IWeir weir)
    {
      if (weir.USNode != null && subnetwork.Nodes.Contains(weir.USNode.Id))
      {
        bool cloneWeir = false;
        if (weir.ToLink != null && subnetwork.Links.Contains(weir.ToLink.Id))
        {
          cloneWeir = true;
        }

        if (!cloneWeir && weir.DSNode != null && subnetwork.Nodes.Contains(weir.DSNode.Id))
        {
          cloneWeir = true;
        }

        if (cloneWeir)
        {
          IWeir weirClone = weir.Clone() as IWeir;
          weirClone.USNode = subnetwork.Nodes[weir.USNode.Id];

          if (weir.ToLink != null)
          {
            weirClone.ToLink = subnetwork.Links[weir.ToLink.Id];
          }

          if (weir.DSNode != null)
          {
            weirClone.DSNode = subnetwork.Nodes[weir.DSNode.Id];
          }

          subnetwork.Add(weirClone);
        }
      }
    }

    private static void CloneOrifice(INetwork subnetwork, IOrifice orifice)
    {
      if (orifice.USNode != null && subnetwork.Nodes.Contains(orifice.USNode.Id))
      {
        bool cloneOrifice = false;
        if (orifice.ToLink != null && subnetwork.Links.Contains(orifice.ToLink.Id))
        {
          cloneOrifice = true;
        }

        if (!cloneOrifice && orifice.DSNode != null && subnetwork.Nodes.Contains(orifice.DSNode.Id))
        {
          cloneOrifice = true;
        }

        if (cloneOrifice)
        {
          IOrifice orificeClone = orifice.Clone() as IOrifice;
          orificeClone.USNode = subnetwork.Nodes[orifice.USNode.Id];

          if (orifice.ToLink != null)
          {
            orificeClone.ToLink = subnetwork.Links[orifice.ToLink.Id];
          }

          if (orifice.DSNode != null)
          {
            orificeClone.DSNode = subnetwork.Nodes[orifice.DSNode.Id];
          }

          subnetwork.Add(orificeClone);
        }
      }
    }

    private static void CloneInfiltrator(INetwork subnetwork, IInfiltrator infiltrator)
    {
      if (infiltrator.Node != null && subnetwork.Nodes.Contains(infiltrator.Node.Id))
      {
        IInfiltrator infiltratorClone = infiltrator.Clone() as IInfiltrator;
        infiltratorClone.Node = subnetwork.Nodes[infiltrator.Node.Id];
        subnetwork.Add(infiltratorClone);
      }
    }

    private static void CloneStorage(INetwork subnetwork, IStorage storage)
    {
      if (storage.Node != null && subnetwork.Nodes.Contains(storage.Node.Id))
      {
        IStorage storageClone = storage.Clone() as IStorage;
        storageClone.Node = subnetwork.Nodes[storage.Node.Id];
        subnetwork.Add(storageClone);
      }
    }

    /// <summary>
    /// Extracts a subnetwork of the network by copying all entities related to the given links into a new Network.
    /// </summary>
    /// <param name="links">The links to extract for making the subnetwork.</param>
    /// <returns>A new Network containing copies of all the specified links and associated entities.</returns>
    private INetwork ExtractSubnetwork(IList<ILink> links)
    {
      INetwork subnetwork = new Network();

      // Copy links, nodes, and cross sections
      foreach (ILink link in links)
      {
        CloneLink(subnetwork, link);
      }

      // Copy directors
      foreach (IDirector director in this.Directors)
      {
        CloneDirector(subnetwork, director);
      }

      // Copy areas
      foreach (IArea area in this.Areas)
      {
        CloneArea(subnetwork, area);
      }

      // Copy weirs
      foreach (IWeir weir in this.Weirs)
      {
        CloneWeir(subnetwork, weir);
      }

      // Copy orifices
      foreach (IOrifice orifice in this.Orifices)
      {
        CloneOrifice(subnetwork, orifice);
      }

      // Copy infiltrators
      foreach (IInfiltrator infiltrator in this.Infiltrators)
      {
        CloneInfiltrator(subnetwork, infiltrator);
      }

      // Copy storages
      foreach (IStorage storage in this.Storages)
      {
        CloneStorage(subnetwork, storage); 
      }

      //// TODO: Copy pumps

      ResetIds(subnetwork);

      return subnetwork;
    }

    private object GetEntityCollection(IEntity entity)
    {
      switch (entity.EntityType)
      {
        case EntityType.Area:
          return this.Areas;
        case EntityType.Node:
          return this.Nodes;
        case EntityType.CrossSection:
          return this.CrossSections;
        case EntityType.Link:
          return this.Links;
        case EntityType.Weir:
          return this.Weirs;
        case EntityType.Orifice:
          return this.Orifices;
        case EntityType.Director:
          return this.Directors;
        case EntityType.Infiltrator:
          return this.Infiltrators;
        case EntityType.Storage:
          return this.Storages;
        default:
          throw new ArgumentException(string.Format("Unsupported entity type: {0}", entity.EntityType));
      }
    }

    private T InternalCreateEntity<T>(int id) where T : IEntity
    {
      IEntity entity = null;

      if (typeof(T) == typeof(INode))
      {
        entity = new Node(id);
      }
      else if (typeof(T) == typeof(ILink))
      {
        entity = new Link(id);
      }
      else if (typeof(T) == typeof(ICrossSection))
      {
        entity = new CrossSection(id);
      }
      else if (typeof(T) == typeof(IWeir))
      {
        entity = new Weir(id);
      }
      else if (typeof(T) == typeof(IOrifice))
      {
        entity = new Orifice(id);
      }
      else if (typeof(T) == typeof(IArea))
      {
        entity = new Area(id);
      }
      else if (typeof(T) == typeof(IDirector))
      {
        entity = new Director(id);
      }
      else if (typeof(T) == typeof(IInfiltrator))
      {
        entity = new Infiltrator(id);
      }
      else if (typeof(T) == typeof(IStorage))
      {
        entity = new Storage(id);
      }
      else
      {
        throw new ArgumentException(string.Format("Unknown entity type: {0}", typeof(T).ToString()));
      }

      return (T)entity;
    }

    private void InitializeNextIds()
    {
      foreach (Type type in this.entityTypes)
      {
        this.nextId[type] = -1;
      }
    }

    private void EntityCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
      // Optimized collections are those with entities that point to other entities. We need to listen to changes
      // in the individual entities to make sure we update the optimization if connectivity changes
      List<object> optimizedCollections = new List<object>() 
        {
          this.Directors, this.Links, this.Weirs, this.Orifices, this.Storages
        };

      // Dependent collections are those with entities that are pointed to by the optimized collections. For these
      // we only need to track if they are added or removed from the collection
      List<object> dependentCollection = new List<object>() { this.Areas, this.Nodes };

      if (optimizedCollections.Contains(sender) || dependentCollection.Contains(sender))
      {
        if (e.Action != NotifyCollectionChangedAction.Move)
        {
          this.isOptimized = false;
        }
      }

      if (optimizedCollections.Contains(sender))
      {
        switch (e.Action)
        {
          case NotifyCollectionChangedAction.Add:
            foreach (IEntity entity in e.NewItems)
            {
              entity.PropertyChanged += this.Entity_PropertyChanged;
            }

            break;
          case NotifyCollectionChangedAction.Remove:
            foreach (IEntity entity in e.OldItems)
            {
              entity.PropertyChanged -= this.Entity_PropertyChanged;
            }

            break;
          case NotifyCollectionChangedAction.Replace:
          case NotifyCollectionChangedAction.Reset:
            foreach (IEntity entity in e.NewItems)
            {
              entity.PropertyChanged += this.Entity_PropertyChanged;
            }

            foreach (IEntity entity in e.OldItems)
            {
              entity.PropertyChanged -= this.Entity_PropertyChanged;
            }

            break;
        }
      }
    }

    private void InitializeOptimizedProperties()
    {
      this.optimizedProperties = new Dictionary<EntityType, List<string>>();
      this.optimizedProperties.Add(EntityType.Director, new List<string>() { "Area", "ToNode" });
      this.optimizedProperties.Add(EntityType.Link, new List<string>() { "DSNode" });
      this.optimizedProperties.Add(EntityType.Weir, new List<string>() { "USNode" });
      this.optimizedProperties.Add(EntityType.Orifice, new List<string>() { "USNode" });
      this.optimizedProperties.Add(EntityType.Storage, new List<string>() { "Node" });
    }

    private void Entity_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
      IEntity entity = sender as IEntity;
      if (entity != null && this.optimizedProperties.ContainsKey(entity.EntityType))
      {
        if (this.optimizedProperties[entity.EntityType].Contains(e.PropertyName))
        {
          this.isOptimized = false;
        }
      }
    }
  }
}
